﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Threading;
using NsbExtensions.Endpoint.Management.Monitoring;
using NsbExtensions.Messages.Management.Monitoring;
using NsbExtensions.Utilities;
using NServiceBus;

namespace NsbExtensions.Endpoint.Management
{
    public class EndpointManagementClientService : TimerServiceBase, IEndpointManagementClientService
    {
        private readonly ReaderWriterLock _probeLock = new ReaderWriterLock();
        private List<IEndpointStatusProbe> _endpointStatusProbes;

        public EndpointManagementClientService() : this(TimeSpan.FromMinutes(5))
        {
        }

        public EndpointManagementClientService(TimeSpan heartbeatInterval)
            : base(heartbeatInterval)
        {
            //If for some reason the heartbeat is set to an interval
            //that it too short and the probing isn't complete,
            //we'll wait to start the next probe until the current
            //one is finished. It wouldn't process
            //more than one status update simultaneously.
            PreventConcurrentIntervalProcessing = true;
        }

        #region IEndpointManagementClientService Members

        public IMessagingEndpoint Endpoint { get; set; }

        public string MonitorServiceAddress { get; set; }

        public TimeSpan StatusReportInterval
        {
            get { return Interval; }
            set { Interval = value; }
        }

        public TimeSpan StatusReportGracePeriod { get; set; }


        public List<IEndpointStatusProbe> EndpointStatusProbes
        {
            get { return _endpointStatusProbes; }
            protected set
            {
                using (new WriterLock(_probeLock, TimeSpan.FromSeconds(5)))
                {
                    if (_endpointStatusProbes == null)
                        _endpointStatusProbes = value;
                    else if (value != null)
                        value.ForEach(AddStatusProbe);
                }
            }
        }

        public void AddStatusProbe(IEndpointStatusProbe probe)
        {
            using (new WriterLock(_probeLock, TimeSpan.FromSeconds(5)))
            {
                if (probe == null)
                    throw new ArgumentNullException("probe");

                if (EndpointStatusProbes == null)
                    EndpointStatusProbes = new List<IEndpointStatusProbe>();
                EndpointStatusProbes.Add(probe);
            }
        }

        #endregion

        protected override void StartService()
        {
            if (Endpoint == null)
                throw new ConfigurationErrorsException(
                    "Endpoint was null. Endpoint MUST be provided with a valid endpoint before starting the EndpointClientManagement service.");

            Endpoint.EndpointOnline += HandleEndpointOnline;
            Endpoint.EndpointOffline += HandleEndpointOffline;
            Endpoint.ServiceException += HandleServiceException;

            base.StartService();
        }

        protected override void ShutDownService()
        {
            base.ShutDownService();

            if (Endpoint != null)
            {
                Endpoint.EndpointOnline -= HandleEndpointOnline;
                Endpoint.EndpointOffline -= HandleEndpointOffline;
                Endpoint.ServiceException -= HandleServiceException;
            }
        }

        protected virtual EndpointOnlineNotification CreateEndpointOnlineNotification()
        {
            DateTime expiration = DateTime.Now.Add(StatusReportInterval).Add(StatusReportGracePeriod).ToUniversalTime();

            return new EndpointOnlineNotification
                       {
                           EndpointID = Endpoint.Metadata.EndpointID,
                           StatusExpirationUtcTime = expiration,
                           StatusTime = DateTime.Now.ToUniversalTime()
                       };
        }

        protected virtual EndpointOfflineNotification CreateEndpointOfflineNotification(string reason,
                                                                                        int
                                                                                            outageDurationInMinutes)
        {
            return new EndpointOfflineNotification
                       {
                           EndpointID = Endpoint.Metadata.EndpointID,
                           OfflineReason = reason,
                           EstimatedOutageDurationInMinutes = outageDurationInMinutes,
                           StatusTime = DateTime.Now.ToUniversalTime()
                       };
        }

        protected virtual EndpointStatusMessage CreateEndpointStatusNotification()
        {
            DateTime expiration = DateTime.Now.Add(StatusReportInterval).Add(StatusReportGracePeriod).ToUniversalTime();

            var measurements = new List<EndpointMeasurement>();

            TakeEndpointMeasurements(measurements);

            return new EndpointStatusUpdateNotification
                       {
                           StatusTime = DateTime.Now.ToUniversalTime(),
                           EndpointID = Endpoint.Metadata.EndpointID,
                           StatusExpirationUtcTime = expiration,
                           EndpointMeasurements = measurements
                       };
        }

        protected virtual void TakeEndpointMeasurements(List<EndpointMeasurement> measurements)
        {
            List<IEndpointStatusProbe> probes = GetStatusProbes();

            foreach (IEndpointStatusProbe probe in probes)
            {
                try
                {
                    EndpointMeasurement measurement = probe.TakeMeasurement(Endpoint);
                    if (measurement != null)
                        measurements.Add(measurement);
                }
                catch (Exception ex)
                {
                    ex =
                        new Exception(
                            "An error occured attempting to take an endpoint status measurement using " + probe != null
                                ? probe.ToString()
                                : " a null proble:" + ex.Message, ex);
                    OnServiceException(ex);
                }
            }
        }

        protected virtual List<IEndpointStatusProbe> GetStatusProbes()
        {
            using (new ReaderLock(_probeLock, TimeSpan.FromSeconds(5)))
            {
                var probes = new List<IEndpointStatusProbe>();
                EndpointStatusProbes.ForEach
                    (
                    probe => probes.Add((IEndpointStatusProbe) probe.Clone())
                    );

                return probes;
            }
        }

        protected virtual void HandleServiceException(object sender, ErrorEventArgs args)
        {
            OnServiceException(args.Exception);
            var errorMessage = new EndpointErrorNotification
                                   {
                                       ErrorMessage = args.Exception.Message
                                   };
            NotifyMonitorService(errorMessage);
        }

        protected virtual void HandleEndpointOffline(object sender, EndpointOfflineEventArgs args)
        {
            EndpointOfflineNotification offlineMessage = CreateEndpointOfflineNotification(args.Reason,
                                                                                           args.EstimatedOutageInMinutes);
            NotifyMonitorService(offlineMessage);
        }

        protected virtual void HandleEndpointOnline(object sender, EndpointEventArgs args)
        {
            EndpointOnlineNotification online = CreateEndpointOnlineNotification();
            NotifyMonitorService(online);
        }

        protected override void PerformTimerAction()
        {
            EndpointStatusMessage status = CreateEndpointStatusNotification();
            NotifyMonitorService(status);
        }

        protected virtual void NotifyMonitorService(IMessage message)
        {
            if (!String.IsNullOrEmpty(MonitorServiceAddress))
                Endpoint.Bus.Send(MonitorServiceAddress, message);
            else
            {
                Endpoint.Bus.Send(message);
            }
        }
    }
}